using Godot;
using Godot.Collections;
using System;
using System.ComponentModel;
using System.Threading;
public partial class VoxelWorld : Node
{
	public const int CHUNK_SIZE = 16;
	static readonly Vector3 CHUNK_MIDPOINT = (new Vector3(0.5f, 0.5f, 0.5f)) * CHUNK_SIZE;
	const int CHUNK_END_SIZE = CHUNK_SIZE - 1;

	public static readonly Vector3I LEFT = new Vector3I(-1, 0, 0);
	public static readonly Vector3I RIGHT = new Vector3I(1, 0, 0);
	public static readonly Vector3I DOWN = new Vector3I(0, -1, 0);
	public static readonly Vector3I UP = new Vector3I(0, 1, 0);
	public static readonly Vector3I FORWARD = new Vector3I(0, 0, 1);
	public static readonly Vector3I BACK = new Vector3I(0, 0, -1);
	public static readonly Vector3I[] DIRECTIONS = [LEFT, RIGHT, DOWN, UP, FORWARD, BACK];

	public static VoxelWorld voxelworld;
	int deleteDistance = 0;
	int rd = 0;
	public int RenderDistance
	{
		get
		{
			return rd;
		}
		set
		{
			rd = value;
			deleteDistance = value + 2;
		}
	}

	public int effectiveRenderDistance = 0;
	Vector3I oldPlayerChunk = new Vector3I();
	bool generating = true;
	bool deleting = false;
	Dictionary<Godot.Vector3I, Chunk> chunks = new Dictionary<Godot.Vector3I, Chunk>();

	private Node settings;
	private CharacterBody3D player;

	public override void _Ready()
	{
		voxelworld = this;
		settings = GetNode<Node>("/root/Settings");
		player = GetNode<CharacterBody3D>("../Player");
	}

	public override void _Process(double delta)
	{
		RenderDistance = (int)settings.Get("render_distance");
		Vector3I playerChunk = (Vector3I)(player.Transform.Origin / CHUNK_SIZE).Round();

		if (deleting || playerChunk != oldPlayerChunk)
		{
			DeleteFarAwayChunks(playerChunk);
			generating = true;
		}
		if (generating == false)
		{
			return;
		}

		playerChunk.Y += (int)Mathf.Round(Mathf.Clamp(player.Velocity.Y, -RenderDistance / 4, RenderDistance / 4));

		for (int x = playerChunk.X - effectiveRenderDistance; x < playerChunk.X + effectiveRenderDistance; x++)
		{
			for (int y = playerChunk.Y - effectiveRenderDistance; y < playerChunk.Y + effectiveRenderDistance; y++)
			{
				for (int z = playerChunk.Z - effectiveRenderDistance; z < playerChunk.Z + effectiveRenderDistance; z++)
				{
					Vector3I chunkPosition = new Vector3I(x, y, z);
					if (playerChunk.DistanceTo(chunkPosition) > RenderDistance)
					{
						continue;
					}
					if (chunks.ContainsKey(chunkPosition))
					{
						continue;
					}

					var chunk = new Chunk();
					chunk.chunkPosition = chunkPosition;
					chunks[chunkPosition] = chunk;
					AddChild(chunk);
					chunk.TryInitialGenerateMesh(chunks);
					foreach (Vector3I dir in DIRECTIONS)
					{
						Chunk neighbor;
						if (chunks.TryGetValue(chunkPosition + dir, out neighbor) && !neighbor.isInitialMeshGenerated)
						{
							neighbor.TryInitialGenerateMesh(chunks);
						}
					}
					return;
				}
			}
		}

		if (effectiveRenderDistance < RenderDistance)
		{
			effectiveRenderDistance += 1;
		}
		else
		{
			generating = false;
		}
	}

	public uint GetBlockInChunk(Vector3I chunkPosition, Vector3I blockSubPosition)
	{
		if (chunks.ContainsKey(chunkPosition))
		{
			Chunk chunk = chunks[chunkPosition];
			if (chunk.data.ContainsKey(blockSubPosition))
			{
				return chunk.data[blockSubPosition];
			}
		}
		return 0;
	}

	public void SetBlockGlobalPosition(Vector3I blockGlobalPosition, uint blockId)
	{
		Vector3I chunkPosition = (Vector3I)((Vector3)blockGlobalPosition / CHUNK_SIZE).Floor();
		if (!chunks.ContainsKey(chunkPosition))
			return;
		Chunk chunk = chunks[chunkPosition];
		Vector3I subPosition = (Vector3I)((Vector3)blockGlobalPosition).PosMod(CHUNK_SIZE);
		if (blockId == 0)
		{
			chunk.data.Remove(subPosition);
		}
		else
		{
			chunk.data[subPosition] = blockId;
		}
		chunk.Regenerate();

		if (Chunk.isBlockTransparent(blockId))
		{
			if (subPosition.X == 0)
			{
				chunks[chunkPosition + LEFT].Regenerate();
			}
			else if (subPosition.X == CHUNK_END_SIZE)
			{
				chunks[chunkPosition + RIGHT].Regenerate();
			}
			if (subPosition.Z == 0)
			{
				chunks[chunkPosition + FORWARD].Regenerate();
			}
			else if (subPosition.Z == CHUNK_END_SIZE)
			{
				chunks[chunkPosition + BACK].Regenerate();
			}
			if (subPosition.Y == 0)
			{
				chunks[chunkPosition + DOWN].Regenerate();
			}
			else if (subPosition.Y == CHUNK_END_SIZE)
			{
				chunks[chunkPosition + UP].Regenerate();
			}
		}
	}

	public void CleanUp()
	{
		foreach (Vector3I chunkPositionKey in chunks.Keys)
		{
			Thread thread = chunks[chunkPositionKey].thread;
			if (thread != null)
			{
				thread.Join();
			}
		}

		chunks = new Dictionary<Vector3I, Chunk>();
		SetProcess(false);

		foreach (Node c in GetChildren())
		{
			c.Free();
		}
	}

	public void DeleteFarAwayChunks(Vector3I playerChunk)
	{
		oldPlayerChunk = playerChunk;
		effectiveRenderDistance = Mathf.Max(1, effectiveRenderDistance - 1);

		int deletedThisFrame = 0;
		int maxDeletions = Mathf.Clamp(2 * (RenderDistance - effectiveRenderDistance), 2, 8);
		foreach (Vector3I chunkPositionKey in chunks.Keys)
		{
			if (playerChunk.DistanceTo(chunkPositionKey) > deleteDistance)
			{
				Thread thread = chunks[chunkPositionKey].thread;
				if (thread != null)
				{
					thread.Join();
				}
				chunks[chunkPositionKey].QueueFree();
				chunks.Remove(chunkPositionKey);
				deletedThisFrame += 1;
				if (deletedThisFrame > maxDeletions)
				{
					deleting = true;
					return;
				}
			}
		}
		deleting = false;
	}
}
